# -*- coding:utf-8 -*-
"""
邮件发送小工具
"""
__author__ = "sunsn"

import os
import smtplib
import logging
import datetime
import traceback
from typing import Dict, List, Sequence
from collections.abc import Iterable
from email.header import Header
from email.mime.text import MIMEText
from email.utils import formataddr, parseaddr
from email.mime.multipart import MIMEMultipart
from email.mime.application import MIMEApplication

import jinja2

logger = logging.getLogger('proj')


class EmailOper(object):
    def __init__(self, email_host: str, email_port: int, email_sender: str, sender_pass: str, sender_alias: str = None):
        """
        配置邮件发送服务
        :param email_host: 发送邮件的主机
        :param email_port: 邮件发送端口
        :param email_sender: 发件人
        :param sender_pass: 发件人密码
        :param sender_alias: 发件人别称
        """
        self.__email_host = email_host
        self.__email_port = email_port
        self.__email_sender = email_sender
        self.__sender_pass = sender_pass
        self.__sender_alias = sender_alias or self.__email_sender.split("@", 1)[0]

    @property
    def email_host(self):
        return self.__email_host

    @email_host.setter
    def email_host(self, value: str):
        self.__email_host = value

    @property
    def email_port(self):
        return self.__email_port

    @email_port.setter
    def email_port(self, value: int):
        self.__email_port = value

    @property
    def email_sender(self):
        return self.__email_sender

    @email_sender.setter
    def email_sender(self, value: str):
        self.__email_sender = value

    @property
    def sender_pass(self):
        return self.__sender_pass

    @sender_pass.setter
    def sender_pass(self, value: str):
        self.__sender_pass = value

    @property
    def sender_alias(self):
        return self.__sender_alias

    @sender_alias.setter
    def sender_alias(self, value: str):
        self.__sender_alias = value

    def build_html_email(self, package_name: str, template_name: str, **context) -> str:
        """
        用法同 flask.render_template:

        build_html_email('ptt_sdk', 'template.html', var1='foo', var2='bar')
        :param package_name:
        :param template_name:
        :param context:
        :return:
        """
        env = jinja2.Environment(
            loader=jinja2.PackageLoader(package_name, 'templates')
        )
        template = env.get_template(template_name)

        return template.render(**context)

    def __format_receivers(self, receivers: Sequence):
        """
        格式化收件人的地址。
        若传入的是字典。应该类似下面的格式：
        {
            “test@123.com”: "测试",
            “admin@123.com”: "管理员"
        }
        若传入的是list/tuple。应该类似下面的格式：
        [“test@123.com”, “admin@123.com”]
        此函数会将输出转化为：
        {
            “test@123.com”: "test",
            “admin@123.com”: "admin"
        }
        :param receivers: 收件人邮箱
        :return:
        """
        assert isinstance(receivers, (list, tuple, dict)), '收件人地址必须以list/tuple或者dict形式传入，以支持多个收件人'
        if isinstance(receivers, dict):
            return receivers
        elif isinstance(receivers, (list, tuple)):
            receivers = {receiver: receiver.split("@", 1)[0] for receiver in receivers}
            return receivers

    def __format_addr(self, address: str, encoding: str = 'utf-8'):
        """
        格式化每个收件地址，以在邮箱中显示姓名
        :param address:
        :param encoding:
        :return:
        """
        name, addr = parseaddr(addr=address)
        return formataddr((Header(name, encoding).encode(), addr))

    def __build_email(self, receivers: Sequence, email_body: str, subject: str, email_type: str = 'plain',
                      encoding: str = 'utf-8', attachments: Sequence = None):
        if not attachments:
            # MIMEText三个参数：第一个为文本内容，第二个 plain 设置文本格式，第三个 utf-8 设置编码
            message = MIMEText(email_body, email_type, encoding)
            message['From'] = formataddr([self.__sender_alias, self.__email_sender])
            receivers = self.__format_receivers(receivers)  # 全部变成字典格式，邮箱：名称
            to_string = ';'.join([self.__format_addr('{0}<{1}>'.format(addr_alias, addr), encoding)
                                  for addr, addr_alias in receivers.items()])
            message['To'] = Header(to_string)
            message['Subject'] = Header(subject, encoding)
        else:
            if isinstance(attachments, str):
                attachments = [attachments]
            elif isinstance(attachments, Iterable):
                attachments = attachments
            else:
                raise Exception("传入的附件，应该是一个文件的全路径，或者列表，包含多个附件的具体路径")
            message = MIMEMultipart()
            # 邮件正文内容
            message.attach(MIMEText(email_body, email_type, encoding))
            # 构造附件
            for attach in attachments:
                with open(attach, 'rb') as f:
                    att = MIMEApplication(f.read())  # 如果只是文本文件可以用MIMEText代替，但像office之类的，需要MIMEApplication
                    att.add_header('Content-Disposition', 'attachment', filename=os.path.basename(attach))
                    message.attach(att)

            message['From'] = formataddr([self.__sender_alias, self.__email_sender])
            receivers = self.__format_receivers(receivers)  # 全部变成字典格式，邮箱：名称
            to_string = ';'.join([self.__format_addr('{0}<{1}>'.format(addr_alias, addr), encoding)
                                  for addr, addr_alias in receivers.items()])
            message['To'] = Header(to_string)
            message['Subject'] = Header(subject, encoding)

        return message

    def send_email(self, receivers: Sequence, email_body: str, subject: str, email_type: str = 'plain',
                   encoding: str = 'utf-8', attachments: Sequence = None):
        """
        发送邮件
        :param receivers: 收件人列表
        :param email_body: 邮件正文
        :param subject: 邮件主题
        :param email_type: 邮件类型：plain 或者 html
        :param encoding: 编码
        :param attachments: 附件全路径列表
        :return:
        """
        obj_smtp = None
        try:
            if self.__email_port == 25:
                obj_smtp = smtplib.SMTP(timeout=10)
            elif self.__email_port == 465:
                obj_smtp = smtplib.SMTP_SSL(timeout=10)
            else:
                raise ValueError("不是标准的邮箱端口：{self.__email_port}, 请确认端口，应该为25或者465")

            obj_smtp.connect(self.__email_host, self.__email_port)
            if self.__sender_pass:
                obj_smtp.login(self.__email_sender, self.__sender_pass)

            # 格式化收件人地址为{addr1:name1, ……}的形式
            receivers = self.__format_receivers(receivers)
            # 构造邮件
            message = self.__build_email(receivers, email_body, subject, email_type, encoding, attachments)
            # 发送邮件
            obj_smtp.sendmail(self.__email_sender, receivers, message.as_string())
        except ValueError as e:
            info = '无法发送邮件。错误信息:{}'.format(traceback.format_exc())
            logger.error(info)
            raise ValueError from e
        except TimeoutError as e:
            info = '无法发送邮件。错误信息:{}'.format(traceback.format_exc())
            logger.error(info)
            raise TimeoutError from e
        finally:
            try:
                if obj_smtp:
                    obj_smtp.quit()
            except Exception as e:
                obj_smtp = None


if __name__ == "__main__":
    EMAIL_HOST = "smtp.qq.com"
    EMAIL_PORT = 465
    EMAIL_SENDER = "shu2015626@qq.com"
    SENDER_PASS = "dsfd"
    SENDER_ALIAS = "shu2015626"
    RECEIVERS = ["shu2015626@qq.com", ]

    obj_email_oper = EmailOper(EMAIL_HOST, EMAIL_PORT, EMAIL_SENDER, SENDER_PASS, SENDER_ALIAS)
    email_body = "测试邮件，勿回！"
    SUBJECT = f"[%s - %s]测试邮件" % ('PyTimedTasks', datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S"))

    obj_email_oper.send_email(
        RECEIVERS, email_body, SUBJECT, 'plain'
    )

